using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.XPath;

using Behemoth.Reflection;
using Behemoth.Web.UI;

using Microsoft.Office.Server.Search.WebControls;

namespace Behemoth.SharePoint.WebParts {

    public enum QueryGenerationMethod {
        NormalFixedQuery = 0,
        ExplicitSqlFixedQuery = 1,
        AppendWhereClauseStuffForm = 2,
        AppendWhereClauseBuildQuery = 3
    }

    [Guid("f480ddc7-d123-4929-815f-81deaedc40e1")]
    public class FlexCoreSearchResults : CoreResultsWebPart {

#warning FlexCoreSearchResults class makes extensive use of reflection and should be used with great care and at your own risk.

        public FlexCoreSearchResults()
            : base() {
            //this.ExportMode = WebPartExportMode.All;
        }

        /// <summary>
        /// A sharepoint search query in SQL syntax
        /// </summary>
        /// <remarks>
        /// When setting this property be sure not to add any ORDER BY clause 
        /// since this is done by private method srho.GetResultsData and so it
        /// can't easily be changed here. :-(
        /// </remarks>
        /// <example>
        /// SELECT Title, Rank, Size, Description, Write, Path FROM portal..scope() WHERE CONTAINS ('"sharepoint"') AND  ( ("SCOPE" = 'All Sites') )
        /// </example>
        protected string FullTextQuery {
            get {
                return _fullTextQuery;
            }
            set {
                _fullTextQuery = value;
                // eliminate any existing keyword query
                if (!string.IsNullOrEmpty(value))
                    this.ResetKQ();
            }
        }
        private string _fullTextQuery;

        [Personalizable(PersonalizationScope.Shared)]
        [WebBrowsable(true)]
        [System.ComponentModel.Category("Advanced 2")]
        [WebDisplayName("Modification Date Field Name")]
        [WebDescription("Allows setting a custom value to take the place of LastModifiedTime' in search query.")]
        public string OverrideLastModifiedTime {
            get { return _overrideLastModifiedTime; }
            set { _overrideLastModifiedTime = value; }
        }
        private string _overrideLastModifiedTime = string.Empty;

        [Personalizable(PersonalizationScope.Shared)]
        [WebBrowsable(true)]
        [System.ComponentModel.Category("Advanced 2")]
        [WebDisplayName("Fixed Keyword Query")]
        [WebDescription("Choose how what is entered into the Fixed Query Text box will be applied to the query.")]
        public QueryGenerationMethod FixedQueryMethod {
            get { return _fixedQueryMethod; }
            set { _fixedQueryMethod = value; }
        }
        private QueryGenerationMethod _fixedQueryMethod = QueryGenerationMethod.NormalFixedQuery;

        [Personalizable(PersonalizationScope.Shared)]
        [WebBrowsable(true)]
        [System.ComponentModel.Category("Advanced 2")]
        [WebDisplayName("Allow QueryString Query Overrides")]
        [WebDescription("If enabled, will allow the sql query or where clause to be provided to the web part by using the 'qQuery' querystring parameter.")]
        public bool AllowQueryStringArgs {
            get { return _allowQueryStringArgs; }
            set { _allowQueryStringArgs = value; }
        }
        private bool _allowQueryStringArgs = false;

        // but, if we already have a helper, let's keep the one we have
        private SearchResultHiddenObjectAccessor _srho_helper = null;
        protected SearchResultHiddenObjectAccessor srho_helper {
            get {
                if (_srho_helper == null)
                    _srho_helper = new SearchResultHiddenObjectAccessor(this);
                return _srho_helper;
            }
        }

        #region Reflection Based Access to Hidden Objects

        /// <summary>
        /// This method is used to set the private fieldName value of _FixedQuery back to null
        /// so that operations in future calls can act normally, since they have some logic
        /// that detects null differently than string.Empty.
        /// </summary>
        private void ResetKQ() {
            // HACK this call uses reflection to reset a private fieldName
            Type t = this.GetType().BaseType; // base type is same as typeof(CoreResultsWebPart)
            object o = Reflector.SetFieldOrProperty(this, t, "_FixedQuery", false, null);
        }

        protected void StuffFormBasedQueryWhereClause(string where) {
            // HACK this call uses reflection to remporily let us stuff values into a read-only collection
            HttpTools.SetFormReadOnly(Page, false);
            this.Page.Request.Form.Add("ASB_ResType_Query", where);
            // this next one is to fool srho.FetchDataFromForm() which seems to require at least one form fieldName besides the query in order for its logic to work 
            this.Page.Request.Form.Add("ASB_TQS_NotQ_tb", "-foobarThisWontHappen");
            HttpTools.SetFormReadOnly(Page, true);
        }

        protected static object GetInternalErrorLevel(ErrorLevel level) {
            Type elType = typeof(CoreResultsWebPart).Assembly.GetType("Microsoft.Office.Server.Search.WebControls.ErrorLevel");
            if (elType == null)
                throw new Exception("Could not get internal enum type 'Microsoft.Office.Server.Search.WebControls.ErrorLevel'.");
            object newEnumValue = Enum.ToObject(elType, level);
            if (newEnumValue == null)
                throw new Exception("Could not generate value for internal enum type 'Microsoft.Office.Server.Search.WebControls.ErrorLevel'.");
            //typeof(CoreResultsWebPart).Assembly.CreateInstance("Microsoft.Office.Server.Search.WebControls.ErrorLevel")
            return newEnumValue;
        }

        protected void IQueryUIError_AddMessage(ErrorLevel level, string message) {
            object newEnumValue = GetInternalErrorLevel(level);
            Type t = typeof(CoreResultsWebPart);
            object o = Reflector.GetFieldOrProperty(this, t, "error", false);
            if (o != null)
                Reflector.InvokeMember(o, "AddMessage", new object[] { newEnumValue, message });
        }
        protected int errorId {
            get {
                Type t = typeof(CoreResultsWebPart);
                object o = Reflector.GetFieldOrProperty(this, t, "errorId", false);
                return (o == null) ? 0 : (int)o;
            }
            set {
                Type t = this.GetType().BaseType; // base type is same as typeof(CoreResultsWebPart)
                object o = Reflector.SetFieldOrProperty(this, t, "errorId", false, null);
            }
        }

        #endregion

        protected virtual string CreateSqlQueryWhereClause() {
            if (!this.AllowQueryStringArgs)
                return string.Empty;
            // first, try to get the query where clause because it is explicitly defined on the QueryString
            string query = Page.Request.QueryString["qQuery"];
            return query;
            /* later this is what we'll do in AR Taxonomy ;-)
            return (string.IsNullOrEmpty(query))
                ? query
                : Facade.ConvertToSharepointQuery(query);
             */
        }

        /// <summary>
        /// This method was used by MS to tranfer web part settings to their own
        /// internal logic class. Unfortunately, they didn't provide much in terms
        /// of ways to access or extend this class, even though it is really useful.
        /// Thus, we resort to reflection in order to make changes to it for our own
        /// ends.
        /// </summary>
        protected override void SetPropertiesOnHiddenObject() {
            // if we have a querystring being passed, let's use it...
            // note that setting FixedQuery is what forces base object(s) to act appropriately
            string query = CreateSqlQueryWhereClause();
            if (!string.IsNullOrEmpty(query))
                this.FixedQuery = query;

            string holdFixedValue = string.Empty;
            if (this.FixedQueryMethod != QueryGenerationMethod.NormalFixedQuery && !string.IsNullOrEmpty(this.FixedQuery)) {
                holdFixedValue = this.FixedQuery;
                switch (this.FixedQueryMethod) {
                    case QueryGenerationMethod.AppendWhereClauseStuffForm:
                        StuffFormBasedQueryWhereClause(this.FixedQuery);
                        ResetKQ(); // HACK this call uses reflection to set a private fieldName value
                        break;
                    case QueryGenerationMethod.AppendWhereClauseBuildQuery:
                    case QueryGenerationMethod.ExplicitSqlFixedQuery:
                        if (this.QueryNumber != QueryId.Query1) {
                            this.FullTextQuery = this.FixedQuery; // note that setting this.FullTextQuery resets the private _FixedQuery too
                        }
                        break;
                }
            }

            // TODO: implement some parameterization of the where clause here, 
            // so we can get some values from different places. For example,
            // we may want to substitue [TODAY] for today's date.

            // Note the above reset of base._FixedQuery happens before any population of 
            // srho occurs. This allows the base method to operate semi-normally, even 
            // though we are hijacking FixedQuery and borrowing it to hold our own info.
            base.SetPropertiesOnHiddenObject();
            srho_helper.OverrideSqlWriteFieldName = this.OverrideLastModifiedTime;

            // If we have a value in holdFixedValue it signifies there may
            // be work to be done now that base.SetPropertiesOnHiddenObject
            // has been called. At the very least we need to put FixedQuery back.
            if (!string.IsNullOrEmpty(holdFixedValue)) {
                switch (this.FixedQueryMethod) {
                    case QueryGenerationMethod.ExplicitSqlFixedQuery:
                        srho_helper.FullTextQuery = this.FullTextQuery;
                        break;
                    case QueryGenerationMethod.AppendWhereClauseBuildQuery:
                        // take the clause from FullTextQuery and create a full SQL query
                        this.FullTextQuery = srho_helper.ComposeFullTextQuery(this.FullTextQuery);
                        srho_helper.FullTextQuery = this.FullTextQuery;
                        break;
                }
                // put FixedQuery property back for the web part properties to make proper use of it
                this.FixedQuery = holdFixedValue;
            }
        }

        protected override void OnInit(EventArgs e) {
            base.OnInit(e);
        }

        /*
        protected override void Render(HtmlTextWriter writer) {
          try {
            writer.Write("Hello World!");
          } catch (Exception ex) {
            writer.Write(ex.Message + " " + ex.StackTrace);
          }
        }
         */

        /// <summary>
        /// Overides the OOB method to allow us to do some nifty magic to by replacing
        /// srho.CoreResults. Microsoft must really hate me right now. :-P
        /// </summary>
        /// <param name="viewPath"></param>
        /// <remarks>
        /// This function is mainly identical to the one in the base class, the main
        /// purpose being to call *our* CoreResults proeprty and thereby call *our*
        /// enhanced version fo GetResultsData, where the properties are configurable.
        /// </remarks>
        /// <returns></returns>
        protected override XPathNavigator GetXPathNavigator(string viewPath) {
            try {
                if (!this.ShowSearchResults) {
                    this.xmlResponseDoc = new XmlDocument();
                    XmlElement newChild = this.xmlResponseDoc.CreateElement("All_Results");
                    this.xmlResponseDoc.AppendChild(newChild);
                    return this.xmlResponseDoc.CreateNavigator();
                }

                // HACK 99% certain that this is already done by this point, but I want to be 100% sure it has been done.
                srho_helper.OverrideSqlWriteFieldName = this.OverrideLastModifiedTime;
                // here is where we call our own method instead of core result, so we can manipulate ALL parts of the query
                this.xmlResponseDoc = srho_helper.CoreResults;

                if (this.xmlResponseDoc != null) {
                    this.IQueryUIError_AddMessage(ErrorLevel.Info, this.xmlResponseDoc.InnerXml);
                    return this.xmlResponseDoc.CreateNavigator();
                }
                return null;
            } catch (Exception exception) {
                string exName = exception.GetType().Name;
                if (exName == "TargetInvocationException")
                    exception = exception.InnerException;
                // thanks to internal types we have to use this ridiculous contraption
                switch (exception.GetType().Name) {
                    case "SrhdcNoKeywordException":
                        this.xmlResponseDoc = new XmlDocument();
                        XmlElement element = this.xmlResponseDoc.CreateElement("All_Results");
                        this.xmlResponseDoc.AppendChild(element);
                        this.bIsNoKeywordException = true;
                        return this.xmlResponseDoc.CreateNavigator();
                    case "SrhdcException":
                        this.IQueryUIError_AddMessage(ErrorLevel.Fatal, exception.Message);
                        this.bIsSrhdcError = true;
                        ; {
                            Type t = exception.GetType();
                            object o = Reflector.GetFieldOrProperty(exception, t, "ErrorCode", true);
                            this.errorId = (o == null) ? 0 : (int)o; // exception.ErrorCode;
                        }
                        if (exception.InnerException != null)
                            this.IQueryUIError_AddMessage(ErrorLevel.Info, exception.ToString());
                        return null;
                    default:
                        this.IQueryUIError_AddMessage(ErrorLevel.Fatal, exception.ToString());
                        return null;
                } // switch
            } // catch
        } // fn

    } // class

} // namespace
